In [1]:
import pandas as pd
import numpy as np
In [2]:
data = pd.read_csv('../data/heloc_dataset_v1.csv')

Zakodowanie zmiennej celu jako 1/0

In [3]:
data['RiskPerformance'] = np.where(data.RiskPerformance=='Bad',1,0)
In [4]:
data.head(2)
Out[4]:
RiskPerformance ExternalRiskEstimate MSinceOldestTradeOpen MSinceMostRecentTradeOpen AverageMInFile NumSatisfactoryTrades NumTrades60Ever2DerogPubRec NumTrades90Ever2DerogPubRec PercentTradesNeverDelq MSinceMostRecentDelq ... PercentInstallTrades MSinceMostRecentInqexcl7days NumInqLast6M NumInqLast6Mexcl7days NetFractionRevolvingBurden NetFractionInstallBurden NumRevolvingTradesWBalance NumInstallTradesWBalance NumBank2NatlTradesWHighUtilization PercentTradesWBalance
0 1 55 144 4 84 20 3 0 83 2 ... 43 0 0 0 33 -8 8 1 1 69
1 1 61 58 15 41 2 4 4 100 -7 ... 67 0 0 0 0 -8 0 -8 -8 0

2 rows × 24 columns

In [5]:
data.shape
Out[5]:
(10459, 24)
In [6]:
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import AdaBoostClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score, accuracy_score, f1_score, precision_score, recall_score
In [7]:
from sklearn.feature_selection import SelectKBest

Podział na train/test

In [8]:
X_train, X_test, y_train, y_test = \
train_test_split(data.drop('RiskPerformance', axis =1), data.RiskPerformance, test_size=.33, random_state=42)

Feature selection na podstawie ANOVA

In [9]:
seletor_f_classif = SelectKBest(k='all')
seletor_f_classif.fit_transform(X_train, y_train)
Out[9]:
array([[ 70, 153,   7, ...,   1,   1,  89],
       [ 64, 103,   1, ...,   3,   2,  58],
       [ 83, 210,   3, ...,   2,   0,  40],
       ...,
       [ 87, 230,   1, ...,   2,   0,  42],
       [ 68, 132,   4, ...,   2,   0,  88],
       [ 70, 130,   5, ...,   1,   0,  75]])
In [10]:
cols_f_classif = pd.DataFrame({
    'column':X_train.columns, 
    'score': seletor_f_classif.scores_, 
    'p_value':seletor_f_classif.pvalues_
}).sort_values('score', ascending = False)
In [11]:
column_list = cols_f_classif.query("score>10").column.values
In [12]:
all_columns = np.concatenate((column_list, ['RiskPerformance']))

Podział na train/test wybranych kolumn

In [13]:
X_train, X_test, y_train, y_test = \
train_test_split(data[column_list], data.RiskPerformance, test_size=.33, random_state=42)

Model

In [14]:
clf_win = LogisticRegression(
        random_state=123,
    max_iter = 200,
    solver = 'liblinear'
    )
clf_win.fit(X_train, y_train)
y_pred = clf_win.predict_proba(X_test)[:,1]
y_pred2 = clf_win.predict(X_test)
print(" AUC = ",round(roc_auc_score( y_test, y_pred)*100,2),
      " ACC = ", round(accuracy_score( y_test, y_pred2)*100,2), 
      " F1 = ", round(f1_score( y_test, y_pred2)*100,2), 
      " Precision = ", round(precision_score( y_test, y_pred2)*100,2), 
      " Recall = ", round(recall_score( y_test, y_pred2)*100,2))
 AUC =  77.04  ACC =  70.83  F1 =  73.08  Precision =  71.12  Recall =  75.15

Wyjaśnienie

In [15]:
from lime import lime_tabular
In [16]:
explainer = lime_tabular.LimeTabularExplainer(
    X_train, 
    feature_names=data[all_columns], 
    class_names=['0','1'], 
    discretize_continuous=False
)

Find interesting observations

In [17]:
y_pred 
Out[17]:
array([0.38039066, 0.30069394, 0.91788581, ..., 0.37389123, 0.85860067,
       0.61332129])
In [18]:
results = pd.DataFrame({'true':y_test, 'pred':y_pred})
results['diff'] = abs(results.true - results.pred)
In [43]:
results.sort_values('diff').loc[results['true']==0]
Out[43]:
true pred diff
3785 0 0.040155 0.040155
9244 0 0.044891 0.044891
6953 0 0.045369 0.045369
613 0 0.050763 0.050763
834 0 0.052615 0.052615
... ... ... ...
168 0 0.968470 0.968470
7689 0 0.969108 0.969108
8470 0 0.976991 0.976991
9059 0 0.986296 0.986296
1039 0 0.986945 0.986945

1633 rows × 3 columns

Obserwacja, która miała najwyższe prawdopodobieństwo bycia w klasie 1

In [40]:
selected_obs = X_test.loc[7585]
exp = explainer.explain_instance(selected_obs, clf_win.predict_proba, num_features=8, top_labels=1)
exp.show_in_notebook(show_table=True, show_all=False)

Obserwacja, która miała najniższe prawdopodobieństwo bycia w klasie 1 (została zaklasyfikowana jako pewne 0)

In [60]:
selected_obs4 = X_test.loc[6409]
exp4 = explainer.explain_instance(selected_obs4, clf_win.predict_proba, num_features=819, top_labels=1)
exp4.show_in_notebook(show_table=True, show_all=False)

Obserwacja, która miała najniższe prawdopodobieństwo bycia w klasie 0 (Została zaklasyfikowana jako pewne 1)

In [56]:
selected_obs2 = X_test.loc[1039]
exp2 = explainer.explain_instance(selected_obs2, clf_win.predict_proba, num_features=19, top_labels=1)
exp2.show_in_notebook(show_table=True, show_all=False)

Obserwacja, która miała najwyższe prawdopodobieństwo bycia w klasie 0

In [23]:
selected_obs3 = X_test.loc[3785]
exp3 = explainer.explain_instance(selected_obs3, clf_win.predict_proba, num_features=8, top_labels=1)
exp3.show_in_notebook(show_table=True, show_all=False)

Pierwszy wykres to true positive, drugi to false positive.

  • Najważniejszą zmienną w obu przypadkach jest NumInqLast6M która mówi o ilości zapytań (coś podobnego do BIK) w ciągu ostatnich 6 miesięcy, a więc można przypuszczać, że w ciągu ostatnich 6 miesięcy tyle razy dany klient próbował wziąć kredyt.
  • Drugą zmienną w obu przypadkach jest wysoko skorelowana z nią zmienna NumInqLast6Mexcl7days, która mówi o ilości zapytań w ciągu ostatnich 6 miesięcy, ale wyłączając ostatnie 7 dni. Warto zauważyć, że obie zmienne niosą bardzo podobną informację (w obu tych przypadkach nawet wartości są takie same), ale zmienne te wpływają bardzo pozytywnie bądź bardzo negatywnie. Co przy regresji jest dość ciekawą obserwacją.
  • Trzecią zmienną jest ExternalRiskEstimate i jest to miara łącząca inne miary mówiące o ryzyku. W obu przypadkach ta wartość jest mniej więcej podobna. Wniosek może być taki, że te miary już są obarczone błędem, przez co nasz model je dubluje.
  • Czwartą zmienną jest NumSatisfactoryTrades, która mówi o ilości transacji pozytywnie zamkniętych( im mniej tym gorzej), w wypadku oby tych obserwacji, działa to na korzyść klasy "0", co jest sprzeczne z intuicją.
  • Kolejną zmienną jest NumTrades60EverDerogPubRec, która mówi ile razy kiedykolwiek ktoś się opóźniał w spłacie więcej niż 60 dni. Oczyiwście im więcej tym gorzej. Druga obserwacja 'False Positive' ma tą wartość jako 0. Więc nie ma przełanek aby ten klient był zły, jednak przez nasz model jest to traktowane na korzyść "złej" klasy.
In [57]:
print('True Positive')
exp.show_in_notebook(show_table=True, show_all=False)
print('False Positive')
exp2.show_in_notebook(show_table=True, show_all=True)
True Positive
False Positive

Pierwsza obserwacja to True Negative, druga False Negative

In [61]:
print('True Negative')
exp3.show_in_notebook(show_table=True, show_all=False)
print('False Negative')
exp4.show_in_notebook(show_table=True, show_all=False)
True Negative
False Negative

How stable are these explanations? - nie bardzo.

Spójrzmy na wyjaśnienia tej samej obserwacji, ale num_features z 18 zmienimy na 19 (o jedną więcej zmiennych)

Zauważmy, że od piątej zmiennej "ważność" i kolejność tych zmiennych się zmienia. A przecież dodałam tylko jedną mienną do wyjaśnień..

In [62]:
exp2 = explainer.explain_instance(selected_obs2, clf_win.predict_proba, num_features=18, top_labels=1)
print(' 18 zmiennych')
exp2.show_in_notebook(show_table=True, show_all=False)
exp22 = explainer.explain_instance(selected_obs2, clf_win.predict_proba, num_features=19, top_labels=1)
print(' 19 zmiennych')
exp22.show_in_notebook(show_table=True, show_all=False)
 18 zmiennych
 19 zmiennych

Model Neural net

In [63]:
from sklearn.neural_network import MLPClassifier
In [64]:
clf = MLPClassifier(alpha=1, max_iter=1000)
In [65]:
clf.fit(X_train, y_train)
Out[65]:
MLPClassifier(activation='relu', alpha=1, batch_size='auto', beta_1=0.9,
              beta_2=0.999, early_stopping=False, epsilon=1e-08,
              hidden_layer_sizes=(100,), learning_rate='constant',
              learning_rate_init=0.001, max_fun=15000, max_iter=1000,
              momentum=0.9, n_iter_no_change=10, nesterovs_momentum=True,
              power_t=0.5, random_state=None, shuffle=True, solver='adam',
              tol=0.0001, validation_fraction=0.1, verbose=False,
              warm_start=False)
In [66]:
y_pred_nn = clf.predict_proba(X_test)

Explain

Weźmy obserwację False Negative. Widać, że zmienne w różnych modelach wskazują na inną klasę, na przykład:

  • PercentTradesWBalance, która mówi o procencie tranckacji dla regresji logistycznej wskazuje na klasę '1', a dla sieci wskazuje na klasę '0' z mocą 0.04,
  • MaxDelqEver, która mówi o maksymalnym opóźnieniu kiedykolwiek, dla regresji logistycznejwskazuje na klasę '0' z mocą 0.04, a dla sieci wskazuje na klasę '1' z mocą 0.03
  • NetFractionInstallBurden również w zależności od modelu wskazuje na różne klasy, jednak "moc" tej zmiennej jest znikoma.
In [72]:
print('Model regresji logistycznej')
exp4.show_in_notebook(show_table=True, show_all=False)
exp_nn = explainer.explain_instance(selected_obs4, clf.predict_proba, num_features=19, top_labels=1)
print('Sieć Neuronowa')
exp_nn.show_in_notebook(show_table=True, show_all=False)